/*
 * File:    Http.java
 * Created: 17-Dec-2006
 * Version: $Id$
 *
 * COPYRIGHT (C) 2006, Bitgate Software, LLC.  All Rights Reserved.
 * RELEASED UNDER THE CREATIVE COMMONS LICENSE.
 *
 * kenji@bitgatesoftware.com
 */

package com.bitgate.util.service.protocol.http;

import static com.bitgate.util.debug.Debug.*;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.channels.Pipe;
import java.nio.channels.SelectionKey;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;

import org.w3c.dom.Node;

import com.bitgate.util.bytes.ByteString;
import com.bitgate.util.node.XPathParser;
import com.bitgate.util.service.ServicePipe;
import com.bitgate.util.service.ServiceVirtualContext;
import com.bitgate.util.service.Worker;
import com.bitgate.util.service.client.ClientContext;
import com.bitgate.util.service.protocol.ProtocolInterface;
import com.bitgate.util.service.protocol.ServerContext;
import com.bitgate.util.service.protocol.http.handler.HandlerException;
import com.bitgate.util.service.protocol.http.handler.HandlerInterface;
import com.bitgate.util.service.protocol.http.logger.Logger;
import com.bitgate.util.socket.AbstractWriteFuture;
import com.bitgate.util.socket.ClientChannel;
import com.bitgate.util.sql.Registry;

/**
 * This is a simple implementation of an HTTP server.  This implementation is here to show how an HTTP server could be
 * implemented in the server environment as it stands.  This code will be updated as time permits.
 * 
 * TODO: Fix so that HttpClientContext is bound to the ClientContext object.
 *
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @version $Id$
 */
public class Http implements ProtocolInterface
{
    private WeakReference<Pipe> commPipe;
    
    private final ServiceVirtualContext vsContext;
    private String serviceId;
    
    /**
     * This is the type of request that was made by the client.  This will then correspond to the correct Http handler
     * code in the {@link com.bitgate.util.service.protocol.http.handler} package of code.  If you plan on adding more
     * Http command handlers, simply add them here, with the appropriate request type.  You can then load in the
     * correct class based on the request that was made by the endpoint client.
     * 
     * @author kenji
     */
    public enum HttpRequestObject {
    	/** Indicates an HTTP GET request. */
    	GET		("GET", "HttpGetHandler"),
    	
    	/** Indicates an HTTP POST request. */
    	POST	("POST", "HttpPostHandler"),
    	
    	/** Indicates an HTTP HEAD request. */
    	HEAD	("HEAD", "HttpHeadHandler");
    	
    	private final String requestType;
    	private final String className;
    	private static final HashMap<String, HandlerInterface> httpRequestClasses = new HashMap<String, HandlerInterface>();
    	
    	private HttpRequestObject(String requestType, String className)
    	{
    		this.requestType = requestType;
    		this.className = className;
    	}
    	
    	/**
    	 * Returns the type of request that was made.
    	 * 
    	 * @return <code>String</code> containing the request type.
    	 */
    	public String getRequestType()
    	{
    		return this.requestType;
    	}
    	
    	/**
    	 * Returns the class name that corresponds to the type of request that was made.
    	 * 
    	 * @return <code>String</code> containing the class name appropriate to the request.
    	 */
    	public String getClassName()
    	{
    		return this.className;
    	}
    	
    	/**
    	 * Returns a {@link HandlerInterface} that handles the type of request that was made.
    	 * 
    	 * @return {@link HandlerInterface} based on the specified request.
    	 * @throws Exception on any errors.
    	 */
    	public HandlerInterface getHandler() throws Exception
    	{
    		synchronized(httpRequestClasses) {
    			if (httpRequestClasses.get(requestType) != null) {
    				return httpRequestClasses.get(requestType);
    			}
    			
				Class<?> returnClass = Class.forName("com.bitgate.util.service.protocol.http.handler." + className);
				HandlerInterface hInterface = (HandlerInterface) returnClass.newInstance();
				
				httpRequestClasses.put(requestType, hInterface);
				
				return hInterface;
    		}
    	}
    }
    
    private static final String HTTP_CLIENT_CONTEXT = "Http.HttpClientContext";
    public static final String HTTP_VIRTUAL_CONTEXT = "Http.ServiceVirtualContext";
    
    public Http()
    {
    	this.vsContext = new ServiceVirtualContext();
    }

    public void init(String assignedServiceId)
    {
    	String topLevelPageTimeout = null;
    	this.serviceId = assignedServiceId;
    	
    	if (isDebugEnabled()) {
    		debug("HTTP protocol service dynamically loaded: Master service ID = '" + serviceId + "'");
    	}
		
    	Node configNode = ServerContext.getDefault().getByServiceId(serviceId);
		int vsEntries = Integer.parseInt(XPathParser.parse(configNode, "count(object[@type='service.virtual-server'])"));
		int finalVsEntries = vsEntries;
		
		if (isDebugEnabled()) {
			debug("Server serial '" + serviceId + "' contains " + vsEntries + " virtual server entries.");
		}
		
		topLevelPageTimeout = XPathParser.parse(configNode, "property[@name='cache.page-timeout']/@value");

		if (topLevelPageTimeout == null || topLevelPageTimeout.equals("")) {
			topLevelPageTimeout = "90m";
		}
		
		if (isDebugEnabled()) {
			debug("Top-level page timeout set to '" + topLevelPageTimeout + "'");
		}
		
		for(int x = 1; x < (vsEntries + 1); x++) {
			String topNode = "object[@type='service.virtual-server'][" + x + "]";
			String serviceVsEnabled = XPathParser.parse(configNode, topNode + "/@enabled");
			String serviceSerialId = XPathParser.parse(configNode, topNode + "/@name");
			String serviceHostname = XPathParser.parse(configNode, topNode + "/property[@name='service.hostname']/@value");
			String serviceDocroot = XPathParser.parse(configNode, topNode + "/property[@name='service.docroot']/@value");
			String serviceDefaultPage = XPathParser.parse(configNode, topNode + "/property[@name='service.default-page']/@value");
			String servicePageTimeout = XPathParser.parse(configNode, topNode + "/property[@name='cache.page-timeout']/@value");
			String serviceSoapRegistry = XPathParser.parse(configNode, topNode + "/property[@name='service.soap-registry']/@value");
			String serviceHtaccess = XPathParser.parse(configNode, topNode + "/property[@name='service.htaccess']/@ref");
			String loggerNode = topNode + "/object[@type='service.logger']";
			String loggerMethod = XPathParser.parse(configNode, loggerNode + "/property[@name='logger.method']/@value");
			String loggerDir = XPathParser.parse(configNode, loggerNode + "/property[@name='logger.directory']/@value");
            
			if (serviceVsEnabled != null && "false".equalsIgnoreCase(serviceVsEnabled)) {
				if (isDebugEnabled()) {
					debug("Virtual Service '" + serviceSerialId + "' for master service '" + serviceId  +
						"' is administratively disabled.");
				}
				
				finalVsEntries--;
				continue;
			}
			
			if (servicePageTimeout == null || servicePageTimeout.equals("")) {
				servicePageTimeout = topLevelPageTimeout;
			}
                                    
			vsContext.addProperty(serviceHostname, "service.serial-id", serviceSerialId);
			vsContext.addProperty(serviceHostname, "service.docroot", serviceDocroot);
			vsContext.addProperty(serviceHostname, "service.default-page", serviceDefaultPage);
			vsContext.addProperty(serviceHostname, "service.soap-registry", serviceSoapRegistry);
			
			if (serviceHtaccess != null && !serviceHtaccess.equals("")) {
				if (Registry.getDefault().get(serviceHtaccess) != null) {
					vsContext.addProperty(serviceHostname, "service.htaccess", serviceHtaccess);
					
					Connection conn = Registry.getDefault().get(serviceHtaccess);
					
					try {
						DatabaseMetaData dbMeta = conn.getMetaData();
				        ResultSet resultSet = dbMeta.getTables(null, null, null, new String[] {"TABLE"});
				        
				        // Get the table names
				        while (resultSet.next()) {
				            debug(" " + resultSet.getString("TABLE_CAT") + ", " + resultSet.getString("TABLE_SCHEM") +
				            		", " + resultSet.getString("TABLE_NAME") + ", " + resultSet.getString("TABLE_TYPE") +
				            		", " + resultSet.getString("REMARKS"));
				        }
				        
				        resultSet.close();
				        conn.close();
					} catch(SQLException e) {
						debug("Exception: " + e.getMessage());
					}
				} else {
					if (isDebugEnabled()) {
						debug("Virtual Service '" + serviceSerialId + "': htaccess '" + serviceHtaccess + "' cannot be used.");
					}
				}
			}
			
			vsContext.addProperty(serviceHostname, "cache.page-timeout", servicePageTimeout);
			vsContext.addProperty(serviceHostname, "logger.method", (loggerMethod != null ? loggerMethod : "screen"));
			vsContext.addProperty(serviceHostname, "logger.directory", loggerDir);
		}
    }

    public void addConnection(ClientContext cContext)
    {
    	if (cContext.getClientChannel().getProperty(HTTP_CLIENT_CONTEXT) != null) {
    		crit("Client ID clash: context already contains HttpClientContext.");
    		return;
    	}
    	
    	cContext.getClientChannel().addProperty(HTTP_CLIENT_CONTEXT, new HttpClientContext(cContext));

    	if (isDebugEnabled()) {
    		debug("Client ID '" + cContext.getSerialId() + "' added to client list.");
    	}
    	
    	cContext.getClientChannel().addProperty(HTTP_VIRTUAL_CONTEXT, vsContext);
    }

    public void removeConnection(ClientContext cContext)
    {
    	cContext.getClientChannel().removeProperty(HTTP_CLIENT_CONTEXT);
    	
    	if (isDebugEnabled()) {
    		debug("Client ID '" + cContext.getSerialId() + "' removed.");
    	}
    }

    public void registerPipe(Pipe pipeObject)
    {
    	this.commPipe = new WeakReference<Pipe>(pipeObject);
    }
    
    public void registerWorker(Worker workerObject)
    {
//    	this.worker = new WeakReference<Worker>(workerObject);
//    	
//    	if (isDebugEnabled()) {
//    		debug("Worker registered.");
//    	}
    }
    
    public void processRead(ClientContext cContext, byte data[])
    {
    	// Because this is stored in the client channel object as a property attachment, we're simply pulling the referenced
    	// object, so we don't have to re-store the object.  We simply modify the reference, and the object will be affected.
    	
    	final HttpClientContext hcContext = (HttpClientContext) cContext.getClientChannel().getProperty(Http.HTTP_CLIENT_CONTEXT);
    	
    	// If the HTTP Client Context is not set, we'll set it here.
    	
    	if (hcContext.getHttpRequestObject() == null) {
    		int crlfIndex = 0;
    		
    		for(int i = 0; i < data.length; i++) {
    			if (data[i] == '\n') {
    				crlfIndex = i;
    				break;
    			}
    		}
    		
    		if (crlfIndex == 0) {
    			warning("Request from '" + cContext + "' made an invalid request: '" + new String(data) + "'");
    			return;
    		}
    		
    		byte[] requestData = new byte[crlfIndex];
    		
    		System.arraycopy(data, 0, requestData, 0, crlfIndex);
    		
    		String requestLine = (new String(requestData)).trim();
    		
    		if (isDebugEnabled()) {
    			debug("Request = '" + requestLine + "'");
    		}
    		
    		if (requestLine.indexOf(" ") != -1) {
    			String requestType = requestLine.substring(0, requestLine.indexOf(" ")).toUpperCase();
    			HttpRequestObject hrType = null;
    			
    			for(HttpRequestObject hrObject : HttpRequestObject.values()) {
    				if (hrObject.getRequestType().equals(requestType)) {
    					hrType = hrObject;
    					break;
    				}
    			}
    			
    			if (hrType == null) {
    				if (isDebugEnabled()) {
    					debug("Unknown request type '" + requestType + "' specified, or not handled.");
    				}
    			} else {
    				if (isDebugEnabled()) {
    					debug("Request type ok, request object set to '" + hrType + "'");
    				}
    			}
    			
    			hcContext.setHttpRequestObject(hrType);
    			hcContext.setOriginalRequest(requestLine);
    			
    			byte[] remainingData = new byte[((data.length - crlfIndex) + 1)];
    			
    			System.arraycopy(data, crlfIndex + 1, remainingData, 0, data.length - (crlfIndex + 1));
    			hcContext.setData(remainingData);
    			
    			// Garbage collect.
    			hrType = null;
    		} else {
    			if (isDebugEnabled()) {
    				debug("Invalid initial request: '" + requestLine + "'");
    			}
    		}
    		
    		// Garbage collect.
    		requestData = null;
    		requestLine = null;
    	} else {
    		hcContext.setData(data);
    	}
    	
    	// We don't do an "else" here, because we could set the object before this point.
    	
    	if (hcContext.getHttpRequestObject() != null) {
			try {
				if (isDebugEnabled()) {
					debug("Completed Requests = " + hcContext.getNumCompletedRequests());
				}
				
				if (hcContext.getHttpRequestObject().getHandler().handleRead(hcContext)) {
					final Pipe currentCommPipe = commPipe.get();
					
					if (isDebugEnabled()) {
						debug("Handle Read indicates data ready to write; registering object to write.");
					}
					
					ServicePipe.registerWrite(cContext, commPipe.get());
		    		
					// Behold, the perfect use of a WriteFuture object.  This takes the header information, and checks it to see
					// if it's keep-alive.  If it is, the WriteFuture clears out the client connect and the headers, and
					// re-registers the object for read.  Otherwise, the client gets disconnected after write.
					
	    			if (hcContext.getHeader(new ByteString("connection")) != null && 
	    				hcContext.getHeader(new ByteString("connection")).toString().toLowerCase().indexOf("keep-alive") != -1) {
	    				hcContext.getClientContext().getClientChannel().setWriteFuture(new AbstractWriteFuture() {
	    					@Override
	    					public void onComplete() {
	    						Logger.logRequest(hcContext, getByteCount());
	    						
	    						int keepAliveTime = 600;
	    						
	    						if (isDebugEnabled()) {
	    							debug("Resetting client context objects, keeping connection alive.");
	    						}
	    						
	    						if (hcContext.getHeader(new ByteString("keep-alive")) != null) {
	    							try {
	    								keepAliveTime = Integer.parseInt(hcContext.getHeader(new ByteString("keep-alive")).toString());
	    							} catch(NumberFormatException e) {
	    								if (isDebugEnabled()) {
	    									debug("Keep-Alive header sent by client doesn't match RFC spec: Keep-Alive = " +
	    											hcContext.getHeader(new ByteString("keep-alive")).toString());
	    								}
	    							}
	    						}
	    						
	    	    				hcContext.reset();
	    	    				hcContext.getClientContext().reset();
	    	    				
	    	    				try {
	    	    					hcContext.getClientContext().getClientChannel().getSocketChannel().socket().setSoTimeout(keepAliveTime * 1000);
	    	    					
	    	    					if (isDebugEnabled()) {
	    	    						debug("Set Socket Timeout to " + keepAliveTime + " second(s).");
	    	    					}
	    	    				} catch(IOException e) {
	    	    					if (isDebugEnabled()) {
	    	    						debug("Unable to set Socket Timeout: " + e.getMessage());
	    	    					}
	    	    				}
	    	    				
	    	    				ServicePipe.registerRead(hcContext.getClientContext(), currentCommPipe);
	    					}
	    				});
	    			} else {
	    				hcContext.getClientContext().getClientChannel().setWriteFuture(new AbstractWriteFuture() {
	    					@Override
	    					public void onComplete() {
	    						Logger.logRequest(hcContext, getByteCount());
	    						
	    						if (isDebugEnabled()) {
	    							debug("Disconnecting serial, connection is not keep-alive (or is keep-alive ignored.)");
	    						}
	    						
	    	    				ServicePipe.disconnectSerial(hcContext.getClientContext(), currentCommPipe);
	    					}
	    				});
	    			}
				}
			} catch(HandlerException e) {
				if (isDebugEnabled()) {
					debug("Handler Exception: " + e.getMessage());
					e.printStackTrace();
				}
			} catch(Exception e) {
				if (isDebugEnabled()) {
					debug("Overall Exception: " + e.getMessage());
					e.printStackTrace();
				}
			}
    	}
    }

    public boolean processWrite(ClientContext cContext)
    {
    	HttpClientContext hcContext = (HttpClientContext) cContext.getClientChannel().getProperty(Http.HTTP_CLIENT_CONTEXT);
    	
    	if (hcContext.getHttpRequestObject() == null) {
    		return false;
    	}
    	
    	try {
    		return hcContext.getHttpRequestObject().getHandler().handleWrite(hcContext);
    	} catch(HandlerException e) {
    		warning("Exception in write processing: " + e.getClass().getName() + ": " + e.getMessage());
    	} catch(Exception e) {
    		warning("Exception in handler: " + e.getClass().getName() + ": " + e.getMessage());
    		e.printStackTrace();
    	}
    	
    	return false;
    }

    public ClientChannel getClientChannel(ClientContext cContext)
    {
    	return cContext.getClientChannel();
    }
    
    public void shutdown()
    {
//    	if (isDebugEnabled()) {
//    		debug("Web services shutting down.");
//    	}
    }

    /**
     * Returns only read operations.
     *
     * @return <code>int</code> containing <code>SelectionKey.OP_READ</code>
     */
    public int getRegisterOps()
    {
    	return SelectionKey.OP_READ;
    }
}
